// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "src/procfs_byte_counter.h"

#include <stdio.h>

#include <glog/logging.h>

namespace cashew {

static const char *kProcNetDevPath = "/proc/net/dev";
static const int kLineBufferSize = 256;

// /proc/net/dev output format
// must remain consistent with kernel (see net/core/dev.c)
static const char *kProcNetDevExpectedLine1 =
    "Inter-|   Receive                            "
    "                    |  Transmit\n";
static const char *kProcNetDevExpectedLine2 =
    " face |bytes    packets errs drop fifo frame "
    "compressed multicast|bytes    packets errs "
    "drop fifo colls carrier compressed\n";
static const char *kProcNetDevLineFormat =
    "%s %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu\n";
static const int kProcNetDevLineFormatNumElements = 17;

// UpdateDelegateWithStats timer interval
// TODO(vlaviano): can reduce this once we implement a smarter ShouldEmitSignal
// policy than 'return true'
static const guint kUpdateDelegateIntervalSeconds = 60;

ProcfsByteCounter::ProcfsByteCounter(const std::string& interface)
    : interface_(interface), proc_net_dev_fp_(NULL), read_stats_source_id_(0),
      counter_rx_bytes_(0), counter_tx_bytes_(0), baseline_stats_(),
      baseline_initialized_(false), counter_changed_since_last_update_(false),
      delegate_(NULL) {
  DCHECK(!interface_.empty());
  // initialize our counter state
  if (!DoReadStats()) {
    LOG(WARNING) << interface_ << ": ctor: DoReadStats failed";
    // fall through so that we'll try again later
  }
  // schedule periodic UpdateDelegateWithStats calls to
  //     update counter state in the future
  read_stats_source_id_ = g_timeout_add_seconds(
      kUpdateDelegateIntervalSeconds,
      StaticUpdateDelegateWithStatsCallback, this);
  if (read_stats_source_id_ == 0) {
    LOG(ERROR) << interface_ << ": ctor: g_idle_add failed";
    return;
  }
}

ProcfsByteCounter::~ProcfsByteCounter() {
  DCHECK(proc_net_dev_fp_ == NULL);
  if (read_stats_source_id_ != 0 && !g_source_remove(read_stats_source_id_)) {
    LOG(WARNING) << interface_ << ": dtor: g_source_remove failed";
  }
  // read and update delegate with most recent stats
  if (!UpdateDelegateWithStats()) {
    LOG(WARNING) << interface_ << ": dtor: UpdateDelegateWithStats failed";
  }
}

const std::string& ProcfsByteCounter::GetInterface() const {
  return interface_;
}

uint64 ProcfsByteCounter::GetRxBytes() const {
  return counter_rx_bytes_;
}

uint64 ProcfsByteCounter::GetTxBytes() const {
  return counter_tx_bytes_;
}

bool ProcfsByteCounter::ReadStats() {
  return DoReadStats();
}

void ProcfsByteCounter::SetDelegate(ByteCounterDelegate *delegate) {
  LOG(INFO) << interface_ << ": SetDelegate";
  delegate_ = delegate;
}

// private methods

bool ProcfsByteCounter::OpenProcNetDev() {
  DCHECK(proc_net_dev_fp_ == NULL);
  if ((proc_net_dev_fp_ = fopen(kProcNetDevPath, "r")) == NULL) {
    PLOG(ERROR) << interface_ << ": OpenProcNetDev: couldn't open "
        << kProcNetDevPath;
    return false;
  }
  return true;
}

void ProcfsByteCounter::CloseProcNetDev() {
  if (proc_net_dev_fp_ != NULL) {
    if (fclose(proc_net_dev_fp_)) {
      PLOG(WARNING) << interface_ << ": CloseProcNetDev: fclose failed";
    }
    proc_net_dev_fp_ = NULL;
  }
  DCHECK(proc_net_dev_fp_ == NULL);
}

// static
gboolean ProcfsByteCounter::StaticUpdateDelegateWithStatsCallback(
    gpointer data) {
  ProcfsByteCounter *counter = reinterpret_cast<ProcfsByteCounter*>(data);
  CHECK_NOTNULL(counter);
  if (!counter->UpdateDelegateWithStats()) {
    LOG(WARNING) << counter->GetInterface()
        << ": StaticUpdateDelegateWithStatsCallback"
        << ": UpdateDelegateWithStats failed";
  }
  return TRUE;  // we want to be called again later
}

bool ProcfsByteCounter::UpdateDelegateWithStats() {
  // read stats
  if (!DoReadStats()) {
    LOG(WARNING) << interface_
        << ": UpdateDelegateWithStats: DoReadStats failed";
    return false;
  }
  // update the delegate with changes
  UpdateDelegate();
  return true;
}

void ProcfsByteCounter::UpdateDelegate() {
  if (delegate_ != NULL && counter_changed_since_last_update_) {
    // reset counter changed flag since we are about to update the delegate
    counter_changed_since_last_update_ = false;

    // update delegate
    delegate_->OnByteCounterUpdate(this, counter_rx_bytes_,
                                   counter_tx_bytes_);

    // NOTE: OnByteCounterUpdate can cause this counter to be deleted, so
    // we're limited in what we can do beyond this point. See this FAQ:
    // http://www.parashift.com/c++-faq-lite/freestore-mgmt.html#faq-16.15
  }
}

bool ProcfsByteCounter::DoReadStats() {
  // we open and close the virtual file each time through, because we get stale
  // data if we keep the file open and rewind/read repeatedly
  if (!OpenProcNetDev()) {
    LOG(ERROR) << interface_ << ": DoReadStats: OpenProcDevNet failed";
    return false;
  }
  char line_buffer[kLineBufferSize];
  for (int line_number = 1;  // use 1-based counting to make log msgs sensible
       fgets(line_buffer, sizeof(line_buffer), proc_net_dev_fp_) != NULL;
       ++line_number) {
    // the first two lines contain human-friendly column headers
    // skip them, while ensuring that they are as we expect
    // this provides some confidence that the format hasn't changed on us
    if (line_number == 1 || line_number == 2) {
      if (strncmp(line_buffer, (line_number == 1) ? kProcNetDevExpectedLine1 :
                  kProcNetDevExpectedLine2, sizeof(line_buffer))) {
        LOG(ERROR) << interface_ << ": DoReadStats: line " << line_number
            << " is not as we expect";
        CloseProcNetDev();
        return false;
      }
      continue;
    }
    // each subsequent line contains the name and stats for a network interface
    // parse it, reading stats, and see if the name matches |interface_|
    //
    // replace first colon in string (immediately after device name) with a
    // space so that sscanf greedy string parsing doesn't cause the colon and
    // possibly the rx byte count following it to all be consumed as part of the
    // device name.
    char *colon_ptr = strchr(line_buffer, ':');
    if (colon_ptr == NULL) {
      LOG(ERROR) << interface_ << ": DoReadStats: line " << line_number
          << ": strchr failed to find colon";
      CloseProcNetDev();
      return false;
    }
    *colon_ptr = ' ';
    char device_name[IFNAMSIZ];
    struct netdev_stats stats;
    memset(&stats, 0, sizeof(stats));
    // cpplint tells us that sscanf, while not prohibited, is sketchy
    // we'll risk it since this data is from the kernel proc fs and we provide
    //   the format string
    //
    // note that some fields of netdev_stats are combined when producing the
    //   contents of /proc/net/dev
    // we assign these combined values to the field named in the column header
    int result = sscanf(line_buffer, kProcNetDevLineFormat,  // NOLINT
                        device_name, &stats.rx_bytes, &stats.rx_packets,
                        &stats.rx_errors, &stats.rx_dropped,
                        &stats.rx_fifo_errors, &stats.rx_frame_errors,
                        &stats.rx_compressed, &stats.multicast, &stats.tx_bytes,
                        &stats.tx_packets, &stats.tx_errors, &stats.tx_dropped,
                        &stats.tx_fifo_errors, &stats.collisions,
                        &stats.tx_carrier_errors, &stats.tx_compressed);
    if (result == EOF && ferror(proc_net_dev_fp_)) {
      PLOG(ERROR) << interface_ << ": DoReadStats: line " << line_number
          << ": sscanf failed";
      CloseProcNetDev();
      return false;
    }
    if (result != kProcNetDevLineFormatNumElements) {
      LOG(ERROR) << interface_ << ": DoReadStats: line " << line_number
          << ": sscanf matched " << ((result == EOF) ? 0 : result)
          << " elements (" << kProcNetDevLineFormatNumElements << " expected)";
      CloseProcNetDev();
      return false;
    }
    if (device_name != interface_) {
      continue;
    }
    // we found a match
    // update our counter based on new stats
    CloseProcNetDev();
    OnStatsUpdate(stats);
    return true;
  }
  LOG(ERROR) << interface_ << ": DoReadStats: no matching device";
  CloseProcNetDev();
  return false;
}

void ProcfsByteCounter::OnStatsUpdate(const struct netdev_stats& stats) {
  LOG(INFO) << interface_ << ": OnStatsUpdate: stats: rx_bytes = "
      << stats.rx_bytes << ", tx_bytes = " << stats.tx_bytes;
  if (baseline_initialized_) {
    LOG(INFO) << interface_ << ": OnStatsUpdate: baseline: rx_bytes = "
        << baseline_stats_.rx_bytes << ", tx_bytes = "
        << baseline_stats_.tx_bytes;
    uint64 old_counter_rx_bytes = counter_rx_bytes_;
    uint64 old_counter_tx_bytes = counter_tx_bytes_;
    LOG(INFO) << interface_ << ": OnStatsUpdate: old counter: rx_bytes = "
        << old_counter_rx_bytes << ", tx_bytes = " << old_counter_tx_bytes;
    // these seemingly naive computations handle wraparound due to the modular
    // nature of unsigned subtraction
    counter_rx_bytes_ += stats.rx_bytes - baseline_stats_.rx_bytes;
    counter_tx_bytes_ += stats.tx_bytes - baseline_stats_.tx_bytes;
    baseline_stats_ = stats;
    LOG(INFO) << interface_ << ": OnStatsUpdate: new counter: rx_bytes = "
        << counter_rx_bytes_ << ", tx_bytes = " << counter_tx_bytes_;
    // check if we have changed counters since last update
    if (!counter_changed_since_last_update_) {
      counter_changed_since_last_update_ =
          counter_rx_bytes_ != old_counter_rx_bytes ||
          counter_tx_bytes_ != old_counter_tx_bytes;
    }
  } else {
    // We initialize the baseline here and update it on on each new sample so
    // that we aren't confused about how many times the device counter has
    // wrapped around since we've started. This works as long as the sample
    // interval is short enough to prevent wrap around between consecutive
    // samples.
    baseline_stats_ = stats;
    baseline_initialized_ = true;
  }
}

}  // namespace cashew
